Skip to content

Go 接口 ​

一个函数只是把结果写出去,为什么参数一定要是文件?如果它只需要“能写入一段字节”,就可以把参数声明成 io.Writer,文件、内存缓冲区和测试替身都能交给它。接口的价值在这里:让调用方只依赖要用的能力。

接口也带来几种需要单独理解的行为:any 能装下整数,不代表能直接做整数运算;一个指针是 nil,放进接口后接口却可能不是 nil;两个接口都装着 slice,比较时可能 panic。这篇从一次写入操作讲起,再把动态类型、断言和 nil 串起来。

遇到的需求对应机制最容易误判的地方
接受不同实现的同一种能力方法接口、隐式实现方法名相同还不够,签名也必须匹配
暂时容纳类型未知的值any它保留实际类型,不会把值变成“通用类型”
想知道里面是什么类型断言、type switch断言是检查现有类型,不是做数值转换
判断是否真的没有值nil 接口有类型的 nil 指针仍会形成非 nil 接口
比较两个接口动态类型与动态值比较动态值不可比较时,比较可能 panic

以下例子实际运行于 Go 1.27.2、Linux/amd64。本文主要讨论可作为普通变量和参数的接口;泛型约束与运行时接口的边界也会交代。文末保留完整程序、测试和故意编译失败的代码。

从 io.Writer 看隐式实现 ​

io.Writer 只有一个方法:[1]

go
type Writer interface {
    Write(p []byte) (n int, err error)
}

一个类型只要方法集满足这个签名,就实现了 io.Writer,不需要写 implements,也不需要修改那个接口所在的包。于是可以写一个只负责生成文本的函数:[2]

go
func WriteCount(w io.Writer, n int) error {
    _, err := fmt.Fprintf(w, "count=%d\n", n)
    return err
}

var buf bytes.Buffer
var text strings.Builder
err1 := WriteCount(&buf, 3)
err2 := WriteCount(&text, 3)
fmt.Printf("%q %q %v %v\n", buf.String(), text.String(), err1, err2)
text
"count=3\n" "count=3\n" <nil> <nil>

*bytes.Buffer 和 *strings.Builder 都有符合要求的 Write 方法。WriteCount 不用知道具体类型,只调用接口提供的写入能力;实现决定把字节存到哪里。写文件时换成满足同一接口的对象即可,不必重写生成文本的逻辑。[3][4]

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    C["调用方选择对象"] --> W["WriteCount<br/>参数 io.Writer"]
    W --> F["fmt.Fprintf<br/>生成 count=3 的文本"]
    F --> M["调用具体对象的 Write"]
    M --> B["*bytes.Buffer<br/>写入内存缓冲"]
    M --> S["*strings.Builder<br/>构建字符串"]
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    C["调用方选择对象"] --> W["WriteCount<br/>参数 io.Writer"]
    W --> F["fmt.Fprintf<br/>生成 count=3 的文本"]
    F --> M["调用具体对象的 Write"]
    M --> B["*bytes.Buffer<br/>写入内存缓冲"]
    M --> S["*strings.Builder<br/>构建字符串"]

图画的是语义上的调用与数据去向,不表示每次调用都会同时写到两个对象。具体实现只有调用方传进来的那一个;编译器如何优化调用,不改变这个约定。

签名满足了,行为约定还要实现者自己守住 ​

下面只有方法名相同,却没有实现接口:

go
type BadWriter struct{}
func (BadWriter) Write([]byte) error { return nil }
var _ io.Writer = BadWriter{}

实际编译诊断中的关键部分是:

text
BadWriter does not implement io.Writer (wrong type for method Write)
    have Write([]byte) error
    want Write([]byte) (int, error)

参数和返回值类型都是接口契约的一部分。参数名可以不同,但不能漏掉返回的字节数,也不能擅自换成另一个返回类型。[2:1]

编译器只能检查类型层面的匹配。io.Writer 文档还规定:返回的 n 小于输入长度时必须有非 nil 错误;不能修改输入字节,也不能保留那份输入 slice。这些行为并不自动由“实现了接口”保证,需要实现者遵守,并由测试检查。[1:1]

实现接口的是 T,还是 *T ​

判断隐式实现时,具体类型也要看完整。值类型 T 的方法集包含值接收者方法,*T 的方法集还包含指针接收者方法。因此下面几种情况不同:[5]

具体类型是否实现 io.Writer原因
bytes.Buffer否Write 是指针接收者方法
*bytes.Buffer是方法集中有 Write([]byte) (int, error)
本文的 WriterFunc是适配器将 Write 定义在值接收者上

即使局部变量 buf 能直接写 buf.Write(p),也不能据此把 buf 值传给 WriteCount。前一种调用利用了可寻址变量自动取址的便利,后一种向接口赋值仍检查值类型的方法集,所以这里传的是 &buf。文末反例明确检查 var _ io.Writer = bytes.Buffer{} 会被拒绝。完整的方法集与调用语法区别见 Go 结构体与方法。[5:1][3:1]

小接口:按调用方的需要收窄能力 ​

如果 WriteCount 的参数写成 *os.File,调用方就得准备文件,测试时也会平白多出文件创建和清理。写成拥有读、写、关闭、定位等一整套方法的大接口,又会要求测试对象实现它根本用不到的方法。io.Writer 只有一个方法,恰好足够完成当前工作。[6]

参数设计调用方要提供什么合适的场景
具体类型 *T这个确定的对象确实依赖该对象特有能力,抽象暂时没有收益
小接口,如 io.Writer当前操作实际需要的方法需要替换实现、隔离外部依赖或编写测试
多方法接口同时具备整组能力的对象调用方确实把这组能力作为一个整体使用
any几乎任意值类型确实未知,后续有明确的识别和处理规则

这里“小”是职责窄,不是机械要求所有接口只能有一个方法。io.ReadWriter 同时需要读和写就很合理。用户代码里的接口通常更适合由使用它的一方定义,方法由实际调用需求决定;不要在没有使用场景时为每个结构体先配一个接口。Go 官方代码审查建议也采用这个方向。[6:1][7]

测试失败路径,不必搭一个真的故障服务 ​

完整代码里定义了一个把函数适配为 Writer 的类型:

go
type WriterFunc func([]byte) (int, error)
func (f WriterFunc) Write(p []byte) (int, error) { return f(p) }

它不是结构体,照样可以有方法并满足接口。让它在每次写入时返回指定错误,就能直接检查失败是否传回调用方:

go
sentinel := errors.New("blocked")
fail := WriterFunc(func([]byte) (int, error) { return 0, sentinel })
fmt.Println("write failure:", errors.Is(WriteCount(fail, 3), sentinel))
text
write failure: true

这里没有创建文件,也没有真的让磁盘发生故障。WriteCount 只需要一个 Writer,测试就能把“写入失败”作为确定输入传给它。文末 TestWriteFailure 实际通过,验证了这条失败路径。类型关系由编译器检查,业务行为由测试检查,两者承担不同工作。[8]

如果生产函数收到接口后立刻断言回某个具体类型,而且没有别的实现真的可用,接口可能没有带来多少好处。此时直接接收具体类型,或者把真正需要的方法补进接口,往往比到处写断言更清楚。这是设计建议,不是语言禁令。

接口值:静态类型规定能做什么,动态类型记录装的是什么 ​

声明 var w io.Writer 时,变量的静态类型就是 io.Writer,编译器据此允许调用 Write。把 &buf 赋给它之后,接口值又有一个动态类型 *bytes.Buffer,以及对应的动态值,也就是那个指针值。[9]

可以用 (动态类型, 动态值) 理解接口的语义:

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    I["变量 w<br/>静态类型 io.Writer"] --> T["动态类型<br/>*bytes.Buffer"]
    I --> V["动态值<br/>指向 buf 的指针"]
    V --> B["具体的 buf 对象"]
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    I["变量 w<br/>静态类型 io.Writer"] --> T["动态类型<br/>*bytes.Buffer"]
    I --> V["动态值<br/>指向 buf 的指针"]
    V --> B["具体的 buf 对象"]

这解释了一个重要限制:即使里面装着 *bytes.Buffer,通过静态类型 io.Writer 也不能直接调用 String(),因为接口没有声明这个方法。若调用方确实需要 String(),应该调整参数契约,或者在确有可选能力需求时使用类型断言。[9:1][10]

接口再赋给接口,不会套一层“接口盒子” ​

go
var buf bytes.Buffer
var w io.Writer = &buf
var x any = w
fmt.Printf("%T %T\n", w, x)
text
*bytes.Buffer *bytes.Buffer

x 的动态类型仍是 *bytes.Buffer,不是 io.Writer。接口之间赋值保留实际的动态类型和值;接口动态类型是具体的非接口类型,不会形成“接口里面套着接口”的层层结构。fmt 的 %T 显示的是这个实际类型。[9:2][4:1]

给接口赋结构体值时,复制的是值;赋指针时,复制的是指针值。结构体里的 slice、map 等引用仍可能共享,所以“放进接口”不是深复制操作。更完整的复制示例见 Go 类型系统 和 Go 结构体与方法。[11]

语义模型和运行时布局要分开看 ​

(type, value) 是理解行为的模型,不是在要求所有编译器永远按同一块内存布局实现。在本次官方 Go 1.27.2 源码中,运行时使用两种结构:[12]

当前运行时结构对应情形包含的信息
eface空接口,如 any类型信息指针 _type,以及 data
iface非空方法接口方法表相关指针 tab,以及 data
itab / abi.ITab非空接口使用的表接口类型、具体类型、方法入口等

data 用于承载动态值的机器表示,不能一律理解成“用户值的所有位直接放在这一格”。装箱、指针和编译优化会影响具体实现。业务代码不应依赖这些私有字段,也不要用 unsafe 手工拆接口来代替正常断言。本文没有测量接口调用成本或堆分配次数,不会从这张表推导“接口必然分配”或“接口必然很慢”。[12:1]

any:不要求方法,但实际类型还在 ​

any 是 interface{} 的别名,两种写法完全等价。空接口不要求方法,所以可以装整数、字符串、结构体、slice 等值;变量的静态类型仍然是接口,不会跟着每次赋值改成动态类型。[13]

go
var x any = 42
n, ok := x.(int)
fmt.Println(n, ok)
s, ok := x.(string)
fmt.Printf("%q %t\n", s, ok)
text
42 true
"" false

要按整数使用它,先检查并取出整数。直接写 x + 1 会编译失败,原因是编译器看到的是 any,无法保证里面是能相加的值。any 把部分检查推迟到了运行时,没有让静态类型规则消失。[13:1][14]

一个 []int 可以放进 any,不等于它就是 []any ​

go
ints := []int{1, 2}
var whole any = ints
values := make([]any, len(ints))
for i, n := range ints { values[i] = n }
ints[0] = 9
fmt.Printf("%T %v | %T %v\n", whole, whole, values, values)
text
[]int [9 2] | []interface {} [1 2]

whole 装的是整份 []int 的 slice 值,底层数组仍共享,所以能看到后来写入的 9。values 则是另一个 slice,其中每个元素都是接口;循环逐项把整数复制了进去,所以还保留 1、2。两种 slice 的元素类型不同,不能直接写 var values []any = ints。文末编译器检查了这个错误。[11:1][15]

any 适合值的种类确实未知的边界,例如通用格式化入口。业务模型已经确定时,用明确字段和明确参数通常更容易检查错误。如果一个算法只需要保留“所有元素是同一种 T”而不是运行时混装各种值,可以进一步考虑泛型,见 Go 泛型。这属于 API 选择,不是说接口和泛型只能二选一。

类型断言与 type switch:检查类型,不是转换类型 ​

先分清两个断言目标 ​

写法成功的条件失败时怎样处理
v := x.(T),T 是具体类型动态类型就是 T运行时 panic
v, ok := x.(T),T 是具体类型动态类型就是 Tok=false,v 为 T 的零值
v, ok := x.(I),I 是接口动态类型实现 I不实现时返回 false,v 为 nil 接口

断言要求 x 是接口表达式。两返回值形式不会因为断言失败而 panic,适合可预期的类型分支;单返回值形式意味着你已经建立了足够强的前提,前提被破坏就让程序失败。[14:1]

go
type UserID int64
var id any = UserID(7)
_, plain := id.(int64)
typed, correct := id.(UserID)
fmt.Println(plain, typed, correct)

var buf bytes.Buffer
var a any = &buf
_, writable := a.(io.Writer)
fmt.Println(writable)
text
false 7 true
true

UserID 与 int64 是不同的定义类型,底层类型一样也不能让第一条断言成功。要转换,应先取出 UserID,再显式转成 int64。后面的断言目标是接口,不要求动态类型名字叫 io.Writer;只要 *bytes.Buffer 实现了它就可以。[14:2][16]

nil 接口没有动态类型,所以对它做具体类型断言同样失败;有类型的 nil 指针却可能成功断言为对应指针类型。这一点在下一节用错误接口验证。

多个类型分支用 type switch ​

go
func describe(x any) string {
    switch v := x.(type) {
    case nil:
        return "nil interface"
    case int:
        return fmt.Sprintf("int %d", v)
    case string:
        return fmt.Sprintf("string %q", v)
    case *Problem:
        if v == nil { return "nil *Problem" }
        return "problem " + v.Message
    default:
        return fmt.Sprintf("other %T", v)
    }
}

依次传入 nil、42、"go"、(*Problem)(nil)、UserID(7),实际输出:

text
nil interface
int 42
string "go"
nil *Problem
other main.UserID

单一类型分支里的 v 具有该分支的类型,因此 case string 可以直接按字符串处理,case *Problem 可以访问字段。case nil 只匹配 nil 接口;带着类型的 nil 指针会进入指针分支。UserID 也不会因为底层是整数就进入 case int。[17]

接口类型也可以出现在 case 中,若多个 case 都可能匹配,按源码顺序选择第一个匹配分支。需要区分具体类型时,把宽泛接口分支放在哪里必须有意识地决定;case any 放在前面会截住后面的所有非 nil 具体值。[17:1]

nil 接口与有类型的 nil:错误处理最常见的陷阱 ​

先定义一个自定义错误。这里特意让方法可以处理 nil 接收者,便于观察而不让程序中途结束:

go
type Problem struct { Message string }
func (p *Problem) Error() string {
    if p == nil { return "<nil *Problem>" }
    return p.Message
}

var p *Problem
var err error = p
var x any = err
fmt.Println(p == nil, err == nil, x == nil)
fmt.Printf("%T %T\n", err, x)
fmt.Println(err.Error())
text
true false false
*main.Problem *main.Problem
<nil *Problem>

p 确实是 nil。把它放进 error 后,接口记录的是 (*Problem, nil),动态类型已经存在,所以接口不等于 nil。再赋给 any 也保留这个动态类型和值,不会自动“清空”。Error 能正常返回,是因为方法显式检查了 nil,不是接口替它做了保护。[18][19]

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    E["var empty error"] --> ET["动态类型:无"]
    E --> EV["动态值:无<br/>empty == nil 为 true"]
    N["var p *Problem = nil<br/>var err error = p"] --> NT["动态类型:*Problem"]
    N --> NV["动态值:nil 指针<br/>err == nil 为 false"]
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    E["var empty error"] --> ET["动态类型:无"]
    E --> EV["动态值:无<br/>empty == nil 为 true"]
    N["var p *Problem = nil<br/>var err error = p"] --> NT["动态类型:*Problem"]
    N --> NV["动态值:nil 指针<br/>err == nil 为 false"]
状态动态类型动态值与 nil 比较
var err error无无true
err = (*Problem)(nil)*Problemnil 指针false
err = &Problem{"failed"}*Problem指向错误对象的指针false
err = nil无无true

err != nil 只说明接口本身不为 nil,不能保证里面的指针非 nil。反过来,调用真正 nil 接口上的方法会 panic,因为它根本没有可调用的动态类型。文末测试分别检查了 nil 接口调用和 typed nil 断言,不把它们混成一种错误。[18:1][10:1]

返回 error 时,成功分支要明确 return nil ​

go
func badResult(ok bool) error {
    var p *Problem
    if !ok { p = &Problem{Message: "failed"} }
    return p
}

func goodResult(ok bool) error {
    if !ok { return &Problem{Message: "failed"} }
    return nil
}

fmt.Println(badResult(true) == nil, goodResult(true) == nil)
text
false true

第一种写法在成功时返回了一个 nil *Problem,函数返回类型却是 error,返回过程中把它装成带类型的接口。调用方按正常的 if err != nil 检查,就误以为失败了。第二种直接返回 nil 接口,才符合 error 约定中的“没有错误”。Go FAQ 用同样的机制解释这一问题。[18:2][20]

最简单的修法通常在生产接口值的地方:成功明确返回 nil,构造函数写清是否可能返回 nil,别把“可选对象”藏进模糊的接口里。不要为了兜底就对每个参数调用 reflect.ValueOf(x).IsNil(),它只支持若干可为 nil 的种类,对整数等值或无效的反射值会 panic。反射不是不看类型就能通用判空的捷径。[21]

接口可以比较,不代表里面的值总能比较 ​

go
var a any = int(1)
var b any = int64(1)
var c any = int(1)
fmt.Println(a == b, a == c)
var s any = []int{1}
fmt.Println(s == nil)
text
false true
false

两个非 nil 接口要相等,动态类型必须相同,动态值也必须相等。int(1) 和 int64(1) 类型不同,所以结果是 false。slice 放进接口后,与 nil 比较是安全的,结果是 false,因为接口有动态类型。[22]

但若两边的动态类型都是 []int,继续比较动态值时会遇到不可比较类型,于是 panic。文末 slice equality 子测试验证了这一点。包含接口字段的结构体、以接口作键的 map,也可能把这类检查推迟到运行时,不能因为外层类型通过编译,就假定所有输入都安全。[22:1]

如果要比较两个序列的元素,应该取出具体序列并使用适合元素类型的比较方法;如果要作为键,应在边界规定允许的键类型。any 提供的是接纳能力,不是相等、排序或计算规则。

泛型约束也写 interface,但不都能创建运行时接口值 ​

下面两种声明都用了 interface,用途不同:[23]

声明能否作为普通参数、变量类型主要用途
interface { Write([]byte) (int, error) }可以用方法表达运行时可替换的能力
interface { ~int | ~int64 }不可以作为类型参数约束,限制允许的类型

带类型项或相应限制的非基本接口只能作为约束使用,不能写 var n Number 然后往里面放整数。本篇反例会得到 cannot use type Number outside a type constraint。泛型怎样用约束统一处理一组类型,放到 Go 泛型 展开。

接口的设计可以从调用点检查:这个函数到底要调用哪些方法?这些方法的行为由谁保证?返回的 nil 是不是接口本身的 nil?如果需要恢复具体类型,是否真的在处理一个可选能力?把这些问题答清楚,接口就能减少依赖,而不是把原本可在编译期发现的错误推到运行时。

复现与验证结果 ​

把下文四份文件按路径保存后运行:

bash
go version
go run .
go test -count=1 -v .
go test -count=1 -race .
go vet .
go build -o invalid.a testdata/compile_fail.go

本次环境为 Go 1.27.2、Linux/amd64、CGO_ENABLED=1。正常程序、6 个 Example、3 个 Test、race、vet 均通过。Example 会逐行核对正文输出;其他测试覆盖 Writer 失败路径、断言 panic、nil 接口调用、slice 接口比较和 typed nil 断言。race 仅覆盖执行到的代码,本篇没有测量性能或证明任意接口实现都没有竞争。[24]

最后一条故意编译失败,退出码为 1,验证五类错误:Writer 签名不符、值类型缺少指针接收者方法、直接给 any 加 1、[]int 直接赋给 []any、把非基本接口当普通变量类型。除前文展示的 Writer 诊断外,其他核心信息为:

text
invalid operation: x + 1 (mismatched types any and untyped int)
cannot use ints (variable of type []int) as []any value in variable declaration
cannot use type Number outside a type constraint: interface contains type constraints

完整可复现代码 ​

go.mod ​

go
module example.com/go-interfaces

go 1.27.0

main.go ​

代码块 · 141 行 · 点击收起 / 展开
go
package main

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"strings"
)

func WriteCount(w io.Writer, n int) error {
	_, err := fmt.Fprintf(w, "count=%d\n", n)
	return err
}

type WriterFunc func([]byte) (int, error)

func (f WriterFunc) Write(p []byte) (int, error) { return f(p) }

var _ io.Writer = WriterFunc(nil)

func writers() {
	var buf bytes.Buffer
	var text strings.Builder
	err1 := WriteCount(&buf, 3)
	err2 := WriteCount(&text, 3)
	fmt.Printf("%q %q %v %v\n", buf.String(), text.String(), err1, err2)
	var w io.Writer = &buf
	var x any = w
	fmt.Printf("%T %T\n", w, x)
	sentinel := errors.New("blocked")
	fail := WriterFunc(func([]byte) (int, error) { return 0, sentinel })
	fmt.Println("write failure:", errors.Is(WriteCount(fail, 3), sentinel))
}

type UserID int64

func assertions() {
	var x any = 42
	n, ok := x.(int)
	fmt.Println(n, ok)
	s, ok := x.(string)
	fmt.Printf("%q %t\n", s, ok)
	var id any = UserID(7)
	_, plain := id.(int64)
	typed, correct := id.(UserID)
	fmt.Println(plain, typed, correct)
	var buf bytes.Buffer
	var a any = &buf
	_, writable := a.(io.Writer)
	fmt.Println(writable)
}

func anySlices() {
	ints := []int{1, 2}
	var whole any = ints
	values := make([]any, len(ints))
	for i, n := range ints {
		values[i] = n
	}
	ints[0] = 9
	fmt.Printf("%T %v | %T %v\n", whole, whole, values, values)
}

type Problem struct{ Message string }

func (p *Problem) Error() string {
	if p == nil {
		return "<nil *Problem>"
	}
	return p.Message
}
func badResult(ok bool) error {
	var p *Problem
	if !ok {
		p = &Problem{Message: "failed"}
	}
	return p
}
func goodResult(ok bool) error {
	if !ok {
		return &Problem{Message: "failed"}
	}
	return nil
}
func nilInterfaces() {
	var p *Problem
	var err error = p
	var x any = err
	fmt.Println(p == nil, err == nil, x == nil)
	fmt.Printf("%T %T\n", err, x)
	fmt.Println(err.Error())
	fmt.Println(badResult(true) == nil, goodResult(true) == nil)
	var empty error
	fmt.Println(empty == nil)
}

func describe(x any) string {
	switch v := x.(type) {
	case nil:
		return "nil interface"
	case int:
		return fmt.Sprintf("int %d", v)
	case string:
		return fmt.Sprintf("string %q", v)
	case *Problem:
		if v == nil {
			return "nil *Problem"
		}
		return "problem " + v.Message
	default:
		return fmt.Sprintf("other %T", v)
	}
}
func switches() {
	for _, v := range []any{nil, 42, "go", (*Problem)(nil), UserID(7)} {
		fmt.Println(describe(v))
	}
}
func comparisons() {
	var a any = int(1)
	var b any = int64(1)
	var c any = int(1)
	fmt.Println(a == b, a == c)
	var s any = []int{1}
	fmt.Println(s == nil)
}

func main() {
	for _, demo := range []struct {
		name string
		run  func()
	}{
		{"writers", writers}, {"assertions", assertions}, {"any slices", anySlices},
		{"nil interfaces", nilInterfaces}, {"switches", switches},
		{"comparisons", comparisons},
	} {
		fmt.Println("==", demo.name, "==")
		demo.run()
	}
}

main_test.go ​

代码块 · 86 行 · 点击收起 / 展开
go
package main

import (
	"errors"
	"testing"
)

func Example_writers() {
	writers()
	// Output:
	// "count=3\n" "count=3\n" <nil> <nil>
	// *bytes.Buffer *bytes.Buffer
	// write failure: true
}
func Example_assertions() {
	assertions()
	// Output:
	// 42 true
	// "" false
	// false 7 true
	// true
}
func Example_anySlices() {
	anySlices()
	// Output:
	// []int [9 2] | []interface {} [1 2]
}
func Example_nilInterfaces() {
	nilInterfaces()
	// Output:
	// true false false
	// *main.Problem *main.Problem
	// <nil *Problem>
	// false true
	// true
}
func Example_switches() {
	switches()
	// Output:
	// nil interface
	// int 42
	// string "go"
	// nil *Problem
	// other main.UserID
}
func Example_comparisons() {
	comparisons()
	// Output:
	// false true
	// false
}

func TestWriteFailure(t *testing.T) {
	sentinel := errors.New("blocked")
	fail := WriterFunc(func([]byte) (int, error) { return 0, sentinel })
	if err := WriteCount(fail, 3); !errors.Is(err, sentinel) {
		t.Fatalf("got %v", err)
	}
}
func TestPanics(t *testing.T) {
	for _, tc := range []struct {
		name string
		run  func()
	}{
		{"failed assertion", func() { var x any = 42; _ = x.(string) }},
		{"nil assertion", func() { var x any; _ = x.(int) }},
		{"nil interface call", func() { var err error; _ = err.Error() }},
		{"slice equality", func() { var a any = []int{1}; var b any = []int{1}; _ = a == b }},
	} {
		t.Run(tc.name, func(t *testing.T) {
			defer func() {
				if recover() == nil {
					t.Fatal("expected panic")
				}
			}()
			tc.run()
		})
	}
}
func TestTypedNilAssertion(t *testing.T) {
	var err error = (*Problem)(nil)
	p, ok := err.(*Problem)
	if !ok || p != nil || err == nil {
		t.Fatal("typed nil semantics changed")
	}
}

testdata/compile_fail.go ​

go
package fail

import (
	"bytes"
	"io"
)

type BadWriter struct{}

func (BadWriter) Write([]byte) error { return nil }

var _ io.Writer = BadWriter{}
var _ io.Writer = bytes.Buffer{}

func invalid() {
	var x any = 1
	_ = x + 1
	ints := []int{1, 2}
	var values []any = ints
	_ = values
}

type Number interface{ ~int | ~int64 }

var n Number

依据 ​

语言规则与本次官方 Go 1.27.2 发行包中的 doc/go_spec.html 核对,接口表示另检查固定版本运行时源码。文中没有把私有布局当成语言承诺。


  1. io.Writer:方法签名、短写必须返回错误、不得修改或保留输入 slice 的约定。 ↩︎ ↩︎

  2. Go 规范:基本接口、实现接口、方法集。 ↩︎ ↩︎

  3. bytes.Buffer.Write、strings.Builder.Write;二者接收者均为指针。 ↩︎ ↩︎

  4. fmt.Fprintf 与 fmt 格式动词:%T 打印 Go 语法形式的类型,%q 显示带转义的字符串。 ↩︎ ↩︎

  5. Go 规范:方法集、调用,自动取址的调用便利不改变值类型的方法集。 ↩︎ ↩︎

  6. Go 官方 Wiki:Interfaces,建议接口通常放在消费方,不要在缺少实际使用场景时预先定义;Effective Go 的接口说明。本篇表中的取舍是依据这些原则给出的设计建议。 ↩︎ ↩︎

  7. io.ReadWriter:组合 Reader 和 Writer 两组方法。 ↩︎

  8. Go 规范:方法声明,方法不局限于结构体类型。 ↩︎

  9. Go 规范:变量的静态类型与动态类型、接口类型;官方文章 The Laws of Reflection 解释接口携带具体类型和值,以及接口不持有接口类型动态值。 ↩︎ ↩︎ ↩︎

  10. Go 规范:选择器,接口可选择其方法集内的方法,nil 接口上调用或求取方法会 panic。 ↩︎ ↩︎

  11. Go 规范:值的表示、赋值。包含引用的值按值复制后仍可能共享数据。 ↩︎ ↩︎

  12. 固定证据为 Go 1.27.2 官方发行包 内 src/runtime/runtime2.go 第 184 行的 iface、第 189 行的 eface,以及 src/internal/abi/iface.go 第 17 行的 ITab。本次已读取这些定义;发行包核对的 SHA-256 为 ecbadb99091a3f46e31f5f934b068b1864eafa7995211b39eaddf76996045fe5。 ↩︎ ↩︎

  13. builtin.any;Go 规范:基本接口,any 是 interface{} 的别名。 ↩︎ ↩︎

  14. Go 规范:类型断言,包含具体类型身份、接口实现、两返回值失败行为。 ↩︎ ↩︎ ↩︎

  15. Go 规范:可赋值性、类型身份。元素类型不同的 slice 不是同一个类型。 ↩︎

  16. Go 规范:类型定义、类型身份。 ↩︎

  17. Go 规范:type switch,分支匹配顺序、nil 分支和分支变量的类型规则。 ↩︎ ↩︎

  18. Go FAQ:为什么 error 是 nil 却不等于 nil;Go 规范:比较运算。 ↩︎ ↩︎ ↩︎

  19. Go 规范:调用,指针接收者可以接收 nil 指针值;具体方法是否解引用由方法代码决定。 ↩︎

  20. builtin.error:Error() string 方法,nil 表示没有错误。 ↩︎

  21. reflect.Value.IsNil:支持的 Kind 及对不支持种类、无效 Value 的 panic 边界。 ↩︎

  22. Go 规范:比较运算、map 类型。动态值不可比较时的接口比较或键操作可能 panic。 ↩︎ ↩︎

  23. Go 规范:一般接口、满足类型约束,非基本接口的使用范围受限。 ↩︎

  24. Go 数据竞争检测器,命令、环境要求及执行路径限制。 ↩︎

保留各书来源与许可 · 示例运行条件以正文为准